package com.hmall.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hmall.common.client.ItemClient;
import com.hmall.common.pojo.Item;
import com.hmall.common.dto.PageDTO;
import com.hmall.pojo.ItemDoc;
import com.hmall.pojo.RequestParams;
import com.hmall.service.SearchService;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SearchServiceImpl implements SearchService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private ItemClient itemClient;

    /**
     * 自动补全
     *
     * @param prefix
     * @return
     */
    @Override
    public List<String> getSuggestion(String prefix) {
        try {
            //1.准备Request
            SearchRequest request = new SearchRequest("hmall");
            //2.准备DSL
            request.source().fetchSource(new String[0],null);
            request.source()//在搜索请求SearchRequest中设置源（source），也就是查询的主体
                    .suggest//向请求中添加一个suggest部分，这将用于生成自动补全建议
                    (
                        new SuggestBuilder()//创建一个SuggestBuilder实例，它是一个帮助类，用于构建和配置建议请求
                                .addSuggestion//向 SuggestBuilder 中添加具体的建议配置，包括建议的名字和如何构建建议
                            (
                            "my_suggestion",//这是你定义的建议名称，稍后你可以通过这个名称来访问建议的结果
                            SuggestBuilders//构建一个完成建议（Completion Suggestion）的查询
                                    .completionSuggestion("suggestion")
                                    // "suggestion" 是一个字段名，它指的是在索引时定义的用于提供自动补全或建议的字段
                                    .size(10)//设置返回建议的数量为10
                                    .skipDuplicates(true)//设置是否跳过重复的建议，这里设为true表示跳过重复项
                                    .prefix(prefix)//设置用于自动补全的前缀，prefix是用户输入的字符串
                            )
                    );
            //3.发请求
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

            //4.解析结果
            //4.1 获取suggestion
            CompletionSuggestion suggestion = response.getSuggest().getSuggestion("my_suggestion");
            //从响应对象中获取suggest部分，然后使用之前定义的建议名称"my_suggestion"来获取具体的建议结果

            //4.2 获取options
            List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
            //从建议结果中获取所有可用的建议选项，它们被封装在CompletionSuggestion.Entry.Option对象中

            //4.3 循环遍历，获取text
            List<String> list = new ArrayList<>(options.size());
            for (CompletionSuggestion.Entry.Option option : options) {
                list.add(option.getText().string());
                //循环遍历所有的选项，获取每个选项的文本内容，并将其转换为字符串，然后添加到列表中
            }
            /*
            CompletionSuggestion.Entry.Option实际上是RestHighLevelClient中
            用来表示自动补全或建议结果中单个选项的类，用于封装一个具体的建议选项。
            这个类包含了关于建议的详细信息，比如建议的文本、得分、相关文档的ID、源文档的部分或全部内容等。
             */
            //使用stream流简化代码
//            List<String> list1 = suggestion.getOptions().stream()
//                    .map(CompletionSuggestion.Entry.Option::getText)//应用一个函数，将每个Option转换为文本
//                    .map(Text::string)//再次应用一个函数，将Text对象转换为字符串
//                    .collect(Collectors.toList());//收集结果到一个新的List中
            return list;//执行搜索请求时，Elasticsearch会返回与指定前缀匹配的建议列表
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 查询过滤
     *
     * @param params
     * @return
     */
    @Override
    public Map<String, List<String>> getFilters(RequestParams params) {
        /*
        此方法用于从Elasticsearch中获取过滤器选项，例如品牌和类别。
        通过执行一个只包含聚合（Aggregations）的查询来实现，而不返回实际的文档数据
         */
        try {

            //1.准备Request
            SearchRequest request = new SearchRequest("hmall");

            //2.准备DSL
            //2.1不要文档数据
            request.source().size(0);
            //设置 size(0) 表示不返回任何文档，只返回聚合结果
            /*
            size参数用于控制搜索结果中返回的文档数量。
            当你设置size为0，这意味着你并不想返回任何文档，尽管你仍然可以执行查询和聚合操作，
            聚合操作是在所有文档上进行的，即使size设置为0，聚合仍然会在所有符合条件的文档上执行，只是不会返回具体的文档结果
             */

            //2.2query条件
            buildBasicQuery(request,params);
            //调用 buildBasicQuery 方法来构建基本的查询条件

            //2.3聚合条件
            request.source()//获取原始数据
                    .aggregation
                    // aggregation是用于定义聚合查询的关键字。
                    /*
                    聚合查询允许你对搜索结果进行分组和统计，而不必返回每一条文档的完整信息
                    通过聚合，你可以获得关于数据的汇总视图，例如：
                    统计某个字段的唯一值数量、计算平均值、找出最大值或最小值、创建直方图、进行地理聚合等
                     */
            (
                    AggregationBuilders
                    //AggregationBuilders是一个静态工厂类，用于构建各种类型的聚合对象
                    /*
                    它提供了一系列静态方法，对应于Elasticsearch DSL中的不同聚合类型，
                    如terms、date_histogram、sum、avg等。
                    这些方法简化了聚合查询的构建过程，
                    能以一种类型安全和直观的方式来定义复杂的聚合查询
                     */
                    .terms("brandAgg")
                    //terms聚合用于对指定字段的值进行分组，计算每个唯一值的频率
                    //这里的"name"参数是指定要进行分组的字段名称。
                    // "brand"字段将被用于分组，计算每个品牌的文档数量
                            .field("brand")
                            //这个参数是在terms聚合中指定的，它定义了要进行分组的字段。
                            // "brand"字段将被用来对品牌进行分组
                            .size(20)
                            //这个参数用于限制返回的分组（桶）的最大数量。最多返回20个不同的品牌桶。
                            // 如果brand字段有超过20个不同的值，那么只有频率最高的20个值会被返回
                    //这段代码：针对品牌 (brand)，每个聚合的大小限制为20个桶（buckets）
            );
            request.source().aggregation(
                    AggregationBuilders.terms("categoryAgg").field("category").size(20)
                    //针对类别 (category)，每个聚合的大小限制为20个桶（buckets）
            );

            //3.发请求
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

            //4.解析结果
            Map<String,List<String>> map = new HashMap<>(2);
            //只有2个聚合字段

            //从响应中提取 Aggregations
            Aggregations aggregations = response
                    .getAggregations();
                    //这是SearchResponse对象上的一个方法，用于获取搜索响应中的聚合结果

            /*
            当你的搜索请求中包含了聚合查询时，聚合结果会被封装在Aggregations对象中。
            通过调用getAggregations()，你可以获取到这个Aggregations对象，进而访问和解析各个聚合的结果
             */

            //4.1根据名称获取品牌聚合
            Terms brandAgg = aggregations.get("brandAgg");//获取了名为brandAgg的聚合结果
            //Terms是一种聚合类型，用于将数据按字段值分组，并统计每个分组的文档数量
            List<String> brandList = new ArrayList<>();
            for (Terms.Bucket bucket : brandAgg.getBuckets()) {
                //brandAgg.getBuckets()则获取了这个聚合结果中所有的桶。
                //在for循环中，Terms.Bucket bucket : brandAgg.getBuckets()迭代了所有桶

                //Terms.Bucket是Terms聚合结果中的一个内部类，表示一个具体的分组。
                // 每个Bucket包含了字段值、文档计数等信息

                //将每个桶（bucket）的键转换为字符串并收集到列表中
                //getKeyAsString()：这是Bucket上的一个方法，用于获取分组的键（即字段值）作为字符串。
                // bucket.getKeyAsString()会返回当前桶所代表的品牌名称
                String key = bucket.getKeyAsString();
                brandList.add(key);
            }
            map.put("brand",brandList);
            //4.2根据名称获取分类聚合
            Terms categoryAgg = aggregations.get("categoryAgg");
            List<String> categoryList = new ArrayList<>();
            for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
                //将每个桶（bucket）的键转换为字符串并收集到列表中
                String key = bucket.getKeyAsString();
                categoryList.add(key);
            }
            map.put("category",categoryList);

            return map;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 分页查询搜索
     *
     * @param params
     * @return
     */
    @Override
    public PageDTO<ItemDoc> search(RequestParams params) {
        try {
            //1.准备Request
            SearchRequest request = new SearchRequest("hmall");

            //2.准备DSL
            //2.1query条件
            buildBasicQuery(request,params);

            //2.2分页
            int page = params.getPage();
            int size = params.getSize();
            request.source().from((page - 1) * size).size(size);

            //2.3排序
            String sortBy = params.getSortBy();
            //得到前端传入排序规则
            if ("sold".equals(sortBy)){
                //如果传入的是销量，降序排列
                request.source().sort(sortBy, SortOrder.DESC);
            }else if ("price".equals(sortBy)){
                //如果传入的是价格，升序排列
                request.source().sort(sortBy,SortOrder.ASC);
            }

            //2.4高亮
            request.source().highlighter(new HighlightBuilder().
                    field("name")
                    //当搜索关键词出现在 "name" 字段中时，高亮显示关键词
                    .requireFieldMatch(false)
                    /*
                    requireFieldMatch(false) 是一个配置选项，用于控制是否要求高亮显示的片段必须完全匹配查询中的所有术语。
                    requireFieldMatch 的值是 true，这意味着高亮显示的片段必须完全匹配整个查询
                    requireFieldMatch 的值是 false 时，Elasticsearch 将会高亮显示任何包含查询中任意一个词的片段，
                    即使这些片段并不完全匹配整个查询。这可以增加高亮显示的覆盖率，确保即使在复杂的查询条件下，
                    相关的文本片段也能被高亮显示出来。
                     */
            );

            //3.发请求
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            //指示客户端使用默认的请求选项来执行搜索请求。如果没有提供请求选项，Elasticsearch 客户端通常也会使用默认选项
            //如果你需要对请求进行更详细的控制，例如设置超时时间、添加自定义的 HTTP 头或者改变请求方法

            //4.解析结果
            SearchHits searchHits = response.getHits();
            //从 SearchResponse 对象中提取 SearchHits 对象。
            // SearchHits 包含了搜索请求的所有命中结果，包括总命中数和实际返回的文档列表

            //4.1 total
            long total = searchHits.getTotalHits().value;
            //getTotalHits() 方法返回一个 TotalHits 对象，该对象表示搜索查询的总命中数。
            // TotalHits 有一个 value 属性，表示实际的命中总数。
            // 告诉有多少文档满足了搜索条件

            //4.2 数据
            SearchHit[] hits = searchHits.getHits();
            //getHits() 方法返回一个 SearchHit 对象的数组，每个 SearchHit 对象代表一个搜索结果中的文档。
            // SearchHit 包含了关于单个文档的信息，
            // 如文档的 _id、_type、得分、源数据（source data）以及任何高亮显示的结果。
            // 这个数组包含了根据分页和大小参数实际返回的文档

            //4.3 遍历
            List<ItemDoc> list = new ArrayList<>();
            for (SearchHit hit : hits) {
                //4.4 获取source：转换为字符串
                String json = hit.getSourceAsString();
                //4.5 转java
                ItemDoc itemDoc = JSON.parseObject(json, ItemDoc.class);
                //4.6 获取高亮
                Map<String, HighlightField> map = hit.getHighlightFields();
                if (map != null && map.size() > 0){
                    HighlightField field = map.get("name");
                    String value = field.getFragments()[0].string();
                    /*
                    field.getFragments() 方法返回一个 Text 对象的数组，每个 Text 对象代表一个高亮显示的片段。
                    通常，如果查询词在字段中出现多次，可能会有多个高亮片段。
                    [0] 表示选取第一个高亮片段。
                    然后，string() 方法被调用来获取这个片段的字符串表示形式
                     */
                    itemDoc.setName(value);
                    // 将高亮显示的片段应用到具体文档对象的过程
                    // 更新文档的显示名称，替换原始字段值，确保在应用程序的各个部分展示的搜索结果都是一致的
                }
                list.add(itemDoc);
            }
            return new PageDTO<>(total,list);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 新增商品
     *
     * @param id
     */
    @Override
    public void saveItemById(Long id) {
        try {
            //1.查询商品数据
            Item item = itemClient.getById(id);

            //转为ItemDoc
            ItemDoc itemDoc = new ItemDoc(item);

            //2.准备Request
            IndexRequest request = new IndexRequest("hmall").id(id.toString());

            //3.准备DSL
            request.source(JSON.toJSONString(itemDoc), XContentType.JSON);

            //4.发送请求
            restHighLevelClient.index(request,RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 删除商品
     *
     * @param id
     */
    @Override
    public void deleteItemById(Long id) {
        try {
            //1.准备Request
            DeleteRequest request = new DeleteRequest("hmall", id.toString());
            //2.发请求
            restHighLevelClient.delete(request,RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void buildBasicQuery(SearchRequest request, RequestParams params) {

        //1.创建布尔查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        /*
        BoolQueryBuilder 允许你结合多种查询类型，
        如 must（所有文档都必须匹配）、
        should（文档至少匹配其中一个）、
        must_not（文档不能匹配）
        和 filter（用于过滤文档，不影响评分），以创建复杂的查询逻辑
         */

        //1.1 key 处理关键字查询
        String key = params.getKey();
        //如果关键词不为空
        if (StringUtils.isNotBlank(key)){
            //非空
            boolQuery.must(QueryBuilders.matchQuery("all",key));
            /*
            这个 "all" 字段通常是一个多字段（multi-field）或者是一个特定的字段，
            它包含了文档中所有可搜索文本的组合。
            当你使用 matchQuery("all", key)，你实际上是在搜索所有这些合并在一起的文本，
            寻找与 key 参数相匹配的任何文本。这可以让你在一个查询中搜索多个字段，
            而不需要显式地为每个字段创建一个 matchQuery
             */
            //matchQuery用于基于文本的搜索，它会分析查询字符串并尝试找到最相关的文档
            //更常见于典型的搜索场景
        }else {
            //空
            boolQuery.must(QueryBuilders
                    /*
                    QueryBuilders 类提供了各种方法来构建这些查询，
                    如 matchQuery、termQuery、rangeQuery、existsQuery 等等，
                    每种方法对应于 Elasticsearch 查询 DSL 中的一种查询类型。
                    使用这些方法可以让你以面向对象的方式在 Java 应用程序中构建 Elasticsearch 查询
                     */
                    .matchAllQuery());
            //matchAllQuery则返回索引中的所有文档，不考虑任何搜索条件
            //更多地用于需要获取所有数据的场合，或者在开发和调试阶段用来检查索引的内容
        }

        //1.2.brand
        String brand = params.getBrand();
        if (StringUtils.isNotBlank(brand)){
            boolQuery.filter(QueryBuilders.termQuery("brand",brand));
            //精确查询
            /*
            精确查询的字段搜是不分词的字段，因此查询的条件也必须是不分词的词条。
            查询时，用户输入的内容跟自动值完全匹配时才认为符合条件
             */
        }

        //1.3.category
        String category = params.getCategory();
        if (StringUtils.isNotBlank(category)){
            boolQuery.filter(QueryBuilders.termQuery("category",category));
        }

        //4.price
        Long maxPrice = params.getMaxPrice();
        Long minPrice = params.getMinPrice();
        if (minPrice != null && maxPrice != null){
            boolQuery.filter(QueryBuilders
                    .rangeQuery("price")
                    //范围查询
                    .gte(params.getMinPrice() * 100)
                    // 这里的gte代表大于等于，gt则代表大于
                    .lte(params.getMaxPrice() * 100));
                    // lte代表小于等于，lt则代表小于
        }

        //2.放入request
        //创建算分函数 FunctionScoreQueryBuilder，用于调整文档的得分
        FunctionScoreQueryBuilder queryBuilder = QueryBuilders.functionScoreQuery
                //FunctionScoreQueryBuilder 是 Elasticsearch 查询 DSL 的一部分，
                // 它允许你基于文档的特定属性或条件来修改文档的相关性得分

                /*
                QueryBuilders 是一个工具类，它提供了一系列静态方法来帮助构建 Elasticsearch 查询 DSL 的 Java 表现形式
                functionScoreQuery 是 QueryBuilders 类中的一个方法，用于构建一个 FunctionScoreQueryBuilder 实例。
                FunctionScoreQueryBuilder 是 Elasticsearch 中 functionScoreQuery 查询的 Java 表现形式，
                它允许你基于文档的某些属性或条件来调整文档的得分，从而影响搜索结果的排序

                QueryBuilders.functionScoreQuery(boolQuery, ...)：这是创建 FunctionScoreQueryBuilder 的方法，
                它接受两个主要参数。
                第一个参数是基础查询，这里是 boolQuery，它定义了哪些文档应该被考虑。
                第二个参数是一个 FilterFunctionBuilder 数组，定义了如何修改这些文档的得分
                返回的是一个 FunctionScoreQueryBuilder 实例，这个实例可以被添加到 SearchRequest 的源中，作为查询的一部分。
                一旦查询被执行，Elasticsearch 将会根据你定义的基础查询来筛选文档，并且根据定义的得分函数来调整这些文档的得分。
                 */
        (
                boolQuery,
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]
                        /*
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]：
                        这是一个数组，用于存放多个 FilterFunctionBuilder。

                        FunctionScoreQueryBuilder 是一个用于构建 functionScoreQuery 的类

                        FilterFunctionBuilder[]被 FunctionScoreQueryBuilder 使用来构建得分函数，
                        每个 FilterFunctionBuilder 实例包含两部分：
                        1.过滤器 (Filter)：一个 QueryBuilder 实例，用于确定哪些文档应该应用这个得分函数。
                        这通常是一个简单的查询，如 termQuery 或 rangeQuery。
                        2.得分函数 (ScoreFunction)：一个函数，用于计算满足过滤器条件的文档的新得分

                        FilterFunctionBuilder[] 是一个 FilterFunctionBuilder 对象的数组。
                        当你有多个得分函数需要应用时，你可以创建多个 FilterFunctionBuilder 实例并将它们放入数组中。
                        FunctionScoreQueryBuilder 的构造函数或方法可以接受这个数组作为参数，
                        从而允许你同时应用多个得分函数
                         */
                {
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder
                                /*
                                这是创建 FilterFunctionBuilder 的构造函数，
                                它接受两个参数。
                                第一个参数是一个查询，用于确定哪些文档应该应用这个函数。
                                第二个参数是一个得分函数，它将应用于通过第一个参数查询筛选出的文档
                                 */
                        (
                                QueryBuilders.termQuery("isAD", true),
                                /*
                                这是一个 termQuery，用于查找 "isAD" 字段值为 true 的文档。
                                这将被用作 FilterFunctionBuilder 的过滤条件
                                定义了哪些文档应该被包括在查询结果中
                                可以是任何类型的查询，比如 match_query、bool_query 等
                                 */
                                ScoreFunctionBuilders.weightFactorFunction(100)
                                //如果某个文档满足了与之关联的过滤条件
                                //那么这个文档的原始得分应该乘以 100 这个权重因子
                        )
                }
        );

        request.source().query(queryBuilder);
        //设置 SearchRequest 的源（source）的查询部分为 queryBuilder。
        // 这里的 queryBuilder 是之前构建的查询，
        // 在执行搜索请求时，Elasticsearch 会使用定义的查询逻辑来筛选和排序文档
    }
}
